Skip to content

Vite 插件

VitePress 处于 Vite 环境下,因此天然支持 Vite 插件。

Teek 有过一个想法,那就是将所有功能完全插件化,通过 NPM下载各个插件来合并成主题:

  • 目录页插件
  • 归档页插件
  • 文章信息插件
  • Footer 插件
  • ...

这完全是可行的,每个插件都是独立的,支持任何 VitePress 项目。

但是目前没有太多精力去实现这个计划,您可以通过 Teek 的按需引入功能(等价于下载插件),来加载自己需要的功能。

在了解 Vite 插件实现之前,建议您先去 Vite 官方文档了解什么是 Vite。

下面介绍在 VitePress 中自定义 Vite 插件的场景。

Vite 插件基础模板

首先介绍 Vite 插件的基础模板:

ts
importtype{Plugin } from"vite";interfaceOptions{}exportdefaultfunctionVitePluginVitePressTemplate(option:Options={}):Plugin{return{name:"vite-plugin-vitepress-template",};}

Vite 插件本质是一个函数,需要返回一个对象,对象的各个 Key 就是 Vite 提供的钩子,比如 transformconfig等,我们需要识别这些钩子分别执行了哪部分逻辑,这样才能针对性的实现自己的功能。

Vite 提供了哪些钩子请看官网 插件 API

扫描项目文件

如果您使用了 Teek 主题,那么在项目启动时,终端会打印:

sh
InjectedSidebarDataSuccessfully.注入侧边栏数据成功!InjectedPermalinksDataSuccessfully.注入永久链接数据成功!InjectedDocAnalysisInfoDataSuccessfully.注入文档分析数据成功!InjectedCataloguesDataSuccessfully.注入目录页数据成功!InjectedpostsDataSuccessfully.注入posts数据成功!

每一行都是一个 Vite 插件输出的内容,这些插件都是去扫描项目的 Markdown 文件,然后生成数据并注入到 VitePress 的 themeConfig中。

扫描项目文件的目的有如下场景:

  • 生成侧边栏:根据 Markdown 文件路径生成侧边栏数据
  • 解析 Markdown 文档的 frontmatter来生成文章信息,或给 Markdown 文件自动添加 frontmatter
  • 解析 Markdown 文档的内容,生成站点信息功能(总字数、文章字数、阅读时长等)
  • ...

这里需要用到 Vite 提供的 config钩子,在解析 VitePress 配置前会调用该钩子,因此我们在这个钩子里执行扫描项目文件的逻辑,最后将数据注入到 VitePress 的 themeConfig中。

ts
importtype{Plugin } from"vite";interfaceOptions{}exportdefaultfunctionVitePluginVitePressDemo(option:Options={}):Plugin&{name:string} {return{name:"vitepress-plugin-demo",config(config:any) {const{site:{themeConfig={} },srcDir,} =config.vitepress;constdata=scanProjectFiles(srcDir);themeConfig.demo =data;},};}constscanProjectFiles=(srcDir:string) =>{};

这里就不详细介绍 scanProjectFiles的逻辑,您可以阅读 Teek 的 Vite 插件源码来了解具体实现。

加载功能组件

开头说的可以将各个功能完全插件化,就是利用插件来往 VitePress 的插槽中插入组件。

Vite 提供的 loadtransformresolveId等钩子,是在访问某个资源的时候被调用,比如在浏览器访问某个页面时,我们可以通过这些钩子拦截到页面的代码,然后进行内容加工再返回给浏览器渲染。

因此当进入 VitePress 页面时,我们可以拦截 VitePress 的 Layout组件,然后将自己实现的组件插入到插槽中,最后返回给浏览器渲染。

VitePress 提供了哪些插槽请看 布局插槽

比如自定义一个组件插入到 Layoutlayout-top插槽中。

ts
constisESM=() =>{returntypeof__filename ==="undefined"||typeof__dirname ==="undefined";};constgetDirname=() =>{returnisESM() ?dirname(fileURLToPath(import.meta.url)) :__dirname;};constcomponentName="MyComponent";constcomponentFile=`${componentName}.vue`;constaliasComponentFile=`${getDirname()}/components/${componentFile}`;constvirtualModuleId="virtual:my-component-option";constresolvedVirtualModuleId=`\0${virtualModuleId}`;exportfunctionVitePluginVitePressMyNotFound(option:NotFoundOption={}):Plugin&{name:string} {return{name:"vite-plugin-vitepress-my-not-found",config() {return{resolve:{alias:{[`./${componentFile}`]:aliasComponentFile,},},};},resolveId(id:string) {if(id ===virtualModuleId) returnresolvedVirtualModuleId;},load(id:string) {if(id ===resolvedVirtualModuleId) return`export default ${JSON.stringify(option)}`;if(id.endsWith("vitepress/dist/client/theme-default/Layout.vue")) {constcode=readFileSync(id,"utf-8");constslotName="layout-top";constslotPosition=`<slot name="${slotName}" />`;constsetupPosition='<script setup lang="ts">';returncode.replace(slotPosition,`<${componentName}><template #${slotName}>${slotPosition}</template></${componentName}>`).replace(setupPosition,`${setupPosition}\nimport ${componentName} from './${componentFile}'`);}},};}
vue
<scriptsetuplang="ts"name="MyComponent">importoption from"virtual:my-component-option";const{label="myComponent"} ={...option };</script><template><div>{{label }}</div></template>

插件通过虚拟模块将 option配置传入到 virtual:my-component-option中,因此可以在组件里引入。虚拟模块的内容请看 Vite 官网 虚拟模块相关说明

上面 index.ts给出的代码模板具有通用性,你只需要:

  • const componentName ="MyComponent";改为要插入的组件名
  • const slotName ="layout-top";改为要插入的插槽名

为什么不用 transform钩子?

transform钩子返回的资源内容已经过 rollup 编译过,不再是源内容,因此无法找到插槽位置,一个解决方案是使用 load钩子。

unbuild 构建

unbuild 是一个用于构建库和工具的现代构建工具,由 UnJS团队开发和维护。它旨在简化构建过程,提供高效的打包和构建功能,特别适用于构建 JavaScript 和 TypeScript 项目。

Teek 使用 unbuild 构建 VitePress 插件,这里仅介绍 unbuild 的 entries配置项,其他 unbuild 的配置项请看 unbuild 文档

警告

如果插件在 node 环境下运行,需要构建为 js相关文件,如果在 client环境下运行,则可以保留 tsvue等文件。

VitePress 的 .vitepress/config.mtsnode环境运行,因此 config.mts文件引入的第三方依赖必须是 js相关文件,在 .vitepress/theme/index.ts文件则可以引入 tsvue等不需要构建的文件。

入口文件

如果插件仅只有一个入口文件 index.ts,则 unbuild 的配置文件内容如下所示:

ts
import{defineBuildConfig } from"unbuild";exportdefaultdefineBuildConfig({entries:["src/index"],});

等于:

ts
import{defineBuildConfig } from"unbuild";exportdefaultdefineBuildConfig({entries:[{builder:"rollup",input:"src/index",outDir:"dist"}],});

后者比较灵活,可以指定输出的位置 outDir

提示

如果您觉得 inputoutDirsrc/indexdist不易于阅读,可以改成 ./src/index./dist

Vue 组件

如果插件有 vue 组件,则 unbuild 的配置文件内容如下所示:

ts
import{defineBuildConfig } from"unbuild";exportdefaultdefineBuildConfig({entries:[{builder:"mkdist",input:"src/components",outDir:"dist/components",pattern:["***.ts"],format:"cjs",loaders:["js"] },{builder:"mkdist",input:"src",outDir:"dist",pattern:["***.css"],loaders:["postcss"] },],});

静态目录

如果插件有一个静态文件目录 assets需要复制到输出目录下,则 unbuild 的配置文件内容如下所示:

ts
import{defineBuildConfig } from"unbuild";exportdefaultdefineBuildConfig({entries:[{builder:"copy",input:"src/assets",outDir:"./dist/assets"}],});

使用 copy功能,input只能是目录。

使用 rollup 插件

如果你需要一些额外的 rollup插件打包,则 unbuild 的配置文件内容如下所示:

ts
import{defineBuildConfig } from"unbuild";importRollupPlugin from"rollup-plugin";exportdefaultdefineBuildConfig({entries:[{builder:"rollup",input:"src",outDir:"dist"}],hooks:{"rollup:options":(_,options) =>{if(Array.isArray(options.plugins)) options.plugins.push(RollupPlugin);},},});

hooks是 unbuild 的一个高级配置项,unbuild 会在指定的阶段调用 hooks中的钩子,和 Vite 插件的钩子函数一样。

比如你希望在构建成功后,将一些文件 copy到输出目录中,则可以使用 hooksbuildEnd钩子,并安装 fs-extra工具实现 copy

ts
import{defineBuildConfig } from"unbuild";import{copy } from"fs-extra";exportdefaultdefineBuildConfig({entries:["src/index"],hooks:{"build:done":async() =>{awaitcopy("src/xx.d.ts","dist/xx.d.ts");},},});

externals

externals是一个数组,用于指定不需要构建的依赖包,它将直接从外部引入,而不是构建到输出目录中。

当你使用了第三方依赖 如 vue、vite 等,需要将这些依赖添加到 externals中,否则它们将被构建到输出目录中,导致依赖非常大。

ts
import{defineBuildConfig } from"unbuild";importRollupPlugin from"rollup-plugin";exportdefaultdefineBuildConfig({externals:["vue","vite"],});

其他项目使用您的插件时,如何确保这些在 externals被排除的依赖正确安装呢?毕竟没有这些依赖,插件将无法运行。

您可以在 package.jsondependencies中添加这些依赖,这些第三方依赖就会跟随你的插件一起安装到项目里。

提示

devDependencies是开发依赖,不会随着插件一起安装到项目里,因此需要您斟酌哪些第三方依赖是运行必须的,则放到 dependencies里,哪些是开发时必须的,则放到 devDependencies里。

最近更新